ChainerとRNNと機械翻訳 - Qiita
自然言語処理とニューラルネット
自然言語処理で主に解析対象となるのは単語の配列や構文木などで、これらの内包する情報を表現するためにrecurrent neural network1やrecursive neural network1などに基づくモデルが頻繁に使われます。これらの最大の特徴はニューラルネットがある種のデータ構造を持っているという点で、
特徴
1レイヤあたりのノードはそれほど多くない代わりにネットワークの接続が複雑
入力されるデータごとにネットワークそのものの形状が変化する
伝統的なfeedforward neural networkを前提としたツールキットでは構築が難しいという問題がありました。
Chainerは、そのような問題を概ね解決してしまう強力なニューラルネットのフレームワークです。...(中略)... 入力データをパースすれば自動的にニューラルネットで解析できてしまう
最近書き溜めていたChainerサンプル集でも言語モデルや単語分割器、翻訳モデルなどを実装していますが、いずれも基本的な部分(コード中のforward関数)は半日、短いものだと1時間くらいあれば実装できてしまいます。
お、サンプルありがたい
本稿では主にrecurrent neural networkについて、そのChainerでの実装方法と、自然言語処理での一応用であるencoder-decoder翻訳モデルの解説を行います。
ありがたい
記事の内容はChainer1.4以前を前提としています。1.5系には様子を見て対応します。
version 1.4 か ....
まあこの記事ではざっくりとやり方を知る、ということで、読み進めよう。
Recurrent Neural Network
最も基本的なrecurrent neural network(以降RNN)は、次の図のような、オーソドックスな3層ニューラルネットに隠れ層のフィードバックが追加されたものです。
https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F100698%2Fb7d34800-5a65-9162-dc6b-ab29b8124daf.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=5bec843ef4c7eef8e7e2bc2e9354f7bf
ふーん。まあこの辺からすでにチンプンカンプンだけどね。なんとなくわかったとしよう。
簡単なモデルですが、後で紹介する翻訳モデルはRNNを組み合わせて作られています。またRNN単体でも、言語モデルに使用すると従来のN-gramモデルを簡単に凌駕する精度を叩き出してくれる優れ物です。
凄そう、というのは分かった
とりあえずここでは「単語IDを入力して、次の単語IDを予測する」RNN言語モデルを考えましょう。
はい
まずモデルを定義します。モデルとは学習可能なパラメータの集合のことで、上図の $ W_{∗∗}がこれに相当します。
なるほど$ W_{xh}, $ W_{hh}, $ W_{hy} のことか
この場合 $ W_{**} は全て線形作用素(行列)なので、chainer.functions内のLinearかEmbedIDを使用します。EmbedIDは入力側がone-hotベクトルの場合のLinearで、ベクトルの代わりに発火している要素のIDを渡すことができます。
LinearかEmbedIDを使うのはふーんという感じ
ベクトルの代わりに発火している要素のIDを渡すことができます。 はよくわからない
次に実際の解析を行うforward関数を定義します。ここでは基本的にはモデルの定義と実際の入力データに従って上図のネットワーク構造を再現し、最終的に求めたい値を計算することになります。
ふーん。
言語モデルの場合は次の式に表す文の結合確率を求めることになります。
なるほど?
これで文の確率を求めることができるようになりました。が、上記にはモデルを学習するための損失関数の計算が含まれていません。
どれくらいフィードバックに回すか?的なものだと察している
今回はsoftmax関数を最終段に使用しているので、chainer.functions.softmax_cross_entropyで正解とのクロスエントロピーを求めて損失関数とします。
これで学習ができるようになりました。
なったらしい。
基本的にChainerの処理はこれだけです。従来はこのようなニューラルネットのためにウンザリするような行数のプログラムを記述していたわけですが、Chainerは面倒な計算がほぼ全てPythonの構文に隠蔽されているので、このように短い記述が可能となります。
便利だ。
Encoder-decoder翻訳モデル
RNNを応用した少し複雑な例として、ニューラルネットを使った機械翻訳の手法であるencoder-decoder翻訳モデルを実装してみます。
これは入力から出力までの全ての過程がニューラルネットで記述された翻訳モデルであり、そのシンプルさに反して従来の翻訳モデルに匹敵する精度を実現するため、発表時には研究者に驚きを以って迎えられました。
すごそう
Encoder-decoder翻訳モデルには色々な亜種がありますが、ここでは私のサンプル集でも実装している下図のモデルを書いてみます。
https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F100698%2F949b1930-5db2-008c-4db1-ad24b2eea4fe.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&w=1400&fit=max&s=2a3e546c23194392acc113366c842ab1
考え方としては簡単で、入力言語側(encoder)と出力言語側(decoder)の2個のRNNを用意して、それを中間ノードで繋ぎ合わせたものです。
ほ〜。2個のRNNを繋いでるのか。
$ iと$ jは埋め込み(embedding)層と呼ばれるもので、次元の圧縮された単語情報を表します。
また入力の単語列が反転していますが、このようにすると実験的に良い翻訳結果が出ることが分かっています。なぜなのかはあまりはっきりと分かっていませんが、encoderとdecoderが変換・逆変換の関係にあるから、という風に解釈されていたりします。
これ本に書いてあったような...
図を見て貰えば分かるのですが、encoder側は実際に損失が計算される位置$ yからかなり遠い場所にあるので、普通の活性化関数ではうまく学習できないという問題がある
普通の活性化関数、とは...?
このようなモデルでは重みの事前学習か、LSTMのような長距離の依存関係を記憶できる素子が必要となります。
これも本に書いてあったような...
読んでないから全くわからんけど...
6章 ゲート付きRNN のなかで語られているわけだけど、もしかしていまこの例でやっているのはゲート付きRNNなのかな? 2個のRNNを用意して、それを中間ノードで繋ぎ合わせたもの この「中間ノード」がゲートのこと?
後々ちゃんと読みてぇ〜
さて、上図や式を見てみると、遷移$ W_{∗∗}が8種類あることが分かります。これらが今回学習するパラメータで、モデルの定義にはこれを列挙します。
code:py
model = FunctionSet(
w_xi = EmbedID(SRC_VOCAB_SIZE, SRC_EMBED_SIZE), # 入力層(one-hot) -> 入力埋め込み層
w_ip = Linear(SRC_EMBED_SIZE, 4 * HIDDEN_SIZE), # 入力埋め込み層 -> 入力隠れ層
w_pp = Linear(HIDDEN_SIZE, 4 * HIDDEN_SIZE), # 入力隠れ層 -> 入力隠れ層
w_pq = Linear(HIDDEN_SIZE, 4 * HIDDEN_SIZE), # 入力隠れ層 -> 出力隠れ層
w_yq = EmbedID(TRG_VOCAB_SIZE, 4 * HIDDEN_SIZE), # 出力層(one-hot) -> 出力隠れ層
w_qq = Linear(HIDDEN_SIZE, 4 * HIDDEN_SIZE), # 出力隠れ層 -> 出力隠れ層
w_qj = Linear(HIDDEN_SIZE, TRG_EMBED_SIZE), # 出力隠れ層 -> 出力埋め込み層
w_jy = Linear(TRG_EMBED_SIZE, TRG_VOCAB_SIZE), # 出力隠れ層 -> 出力隠れ層
)
FunctionSet はver2系になったときに削除されている!
このコミットでは ~chainer.Chain か ~chainer.ChainList を使って〜と書いてある
こいつらがまだあるかは分からんけどね
注意する必要があるのはLSTMの入力になる$ W_{ip},$ W_{pp},$ W_{pq},$ W_{yq},$ W_{qq}の各パラメータで、LSTMに入力される側の次元を4倍に増やす必要があります。Chainerの実装しているLSTMは通常の入力の他にinput gate, output gate, forget gateの3種類の入力があり、これを1個のベクトルとしてまとめているためにこのような実装が必要となっています。
4倍に増やす、なるほど。
Chainerの実装しているLSTMは通常の入力の他にinput gate, output gate, forget gateの3種類の入力があり、
functions.lstm
コードコメントに色々書いてある
code:txt
- :math:a : sources of cell input
- :math:i : sources of input gate
- :math:f : sources of forget gate
- :math:o : sources of output gate
exampleも紹介されていた。4倍してるね。
code:py
>> n_units = 100
>> y = chainer.Variable(np.zeros((1, n_units), np.float32))
>> h = chainer.Variable(np.zeros((1, n_units), np.float32))
>> c = chainer.Variable(np.zeros((1, n_units), np.float32))
>> model = chainer.Chain()
>> with model.init_scope():
... model.w = L.Linear(n_units, 4 * n_units)
... model.v = L.Linear(n_units, 4 * n_units)
>> x = model.w(y) + model.v(h)
>> c, h = F.lstm(c, x)
注釈に
最近の実装であるchainer.linksにはこの辺りの実装を隠蔽したバージョンのLSTMが実装されている。
links.lstm のコードの中では functions.lstm をimportしていて
code:py
# def __init__
with self.init_scope():
self.upward = linear.Linear(in_size, 4 * out_size, initialW=0)
self.lateral = linear.Linear(out_size, 4 * out_size, initialW=0,
nobias=True)
# def _initialize_params
for i in six.moves.range(0, 4 * self.state_size, self.state_size):
a, i, f, o = lstm._extract_gates(
self.upward.b.array.reshape(1, 4 * self.state_size, 1))
こんな感じで各所で良い感じに4倍されるようになっていて、呼び出し元からは気にしなくて良くなっている
次にforward関数を書きます。LSTMは内部状態を持っているので、$ pと$ qの計算時にもう一つVariableが必要となることに注意します。
なるほどわからん
注意深く読むと上図の矢印とコードの各部分が対応していることが分かると思います。
あとはこのコードの外側にRNNと同じような学習器を追加すれば、晴れて自前の翻訳データを学習できるようになるわけです。
コードは後々読むとして、 あとはこのコードの外側にRNNと同じような学習器を追加すれば はどういうこっちゃ?
モデルを学習するための損失関数 を追加する、ということかな?
果たしてこのモデルがどのように学習するかですが、...(中略)... 語彙数2000、埋め込み層100、隠れ層100として1万文程度を学習すると、概ね世代ごとに次のような翻訳結果が得られます
結構なデータっすなぁ...
翻訳結果、面白いなぁ
code:txt
入力: 休暇 は いかが で し た か 。
出力:
1: the is is a of of <unk> .
2: the 't is a <unk> of <unk> .
3: it is a good of the <unk> .
4: how is the <unk> to be ?
5: how do you have a <unk> ?
6: how do you have a <unk> ?
7: how did you like the <unk> ?
8: how did you like the weather ?
9: how did you like the weather ?
10: how did you like your work ?
11: how did you like your vacation ?
12: how did you like your vacation ?
13: how did you the weather to drink ?
14: how did you like your vacation ?
15: how did you like your vacation ?
16: how did you like your vacation ?
17: how did you like your vacation ?
18: how did you like your vacation ?
19: how did you enjoy your vacation ?
20: how did you enjoy your vacation now ?
21: how did you enjoy your vacation for you ?
22: how did you enjoy your vacation ?
結果を見て分かるのは、まず大まかな文法や意味の広い単語を生成するよう学習し、次第に具体的な単語を当てに行くよう調整されてゆく点です。これは、ニューラルネットの収束が進むにつれて単語同士の意味の違いをはっきりと捉えることができるようになってゆくためである、と考えることもできます。
なるほどね〜